<!DOCTYPE html>
<html class="no-js" lang="zh">
  <head>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="alternate" href="https://www.desgard.com" hreflang="pt-BR">
<link href="http://gmpg.org/xfn/11" rel="profile">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="theme-color" content="#005344">
<title>Guardia · 瓜地</title>
<meta name="google-site-verification" content="zS1dSn20XtA4FJYEOQLXqI0boxZdMnJ2g3beje-cl20">
<meta name="description" content="I write many code to write less code.💻">
<meta name="keywords" content="">
<!-- Social: Facebook / Open Graph -->
<meta property="og:url" content="https://www.desgard.com/BannerHoverView/">
<meta property="og:title" content="       BannerHoverView - 解耦 TableView Header 实现悬停 | Gua  ">
<meta property="og:description" content="I write many code to write less code.💻">
<meta property="og:site_name" content="Desgard_Duan">
<meta property="og:locale" content="pt_BR">
<meta property="og:type" content="website">
<meta property="og:author" content="https://www.facebook.com/desgard.duan">
<meta property="og:image" content="https://www.desgard.com">
<!-- Social: Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@nandomoreirame">
<meta name="twitter:domain" content="https://www.desgard.com">
<meta name="twitter:title" content="       BannerHoverView - 解耦 TableView Header 实现悬停 | Gua  ">
<meta name="twitter:description" content="I write many code to write less code.💻">
<meta name="twitter:image:src" content="https://www.desgard.com">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="114x114" href="https://www.desgard.com/assets/ico/apple-touch-icon-114-516f4e19976b9e4dbb77ad9b576831fe.png">
<link rel="apple-touch-icon" sizes="72x72" href="https://www.desgard.com/assets/ico/apple-touch-icon-72-5409b2df229305703caf583d86c845ab.png">
<link rel="apple-touch-icon" href="https://www.desgard.com/assets/ico/apple-touch-icon-57-aa873e019cf659e0d4e6a0b5bb9f379d.png">
<link rel="shortcut icon" href="https://www.desgard.com/assets/ico/favicon-4298be3d3fbe23e18d65bded9d930899.png">
<!-- rel prev and next -->
<!-- Canonical link tag -->
<link rel="canonical" href="https://www.desgard.com/BannerHoverView/">
<link rel="alternate" type="application/rss+xml" title="Gua" href="https://www.desgard.com/feed.xml">
<link rel="stylesheet" href="/assets/main-0b7b828712f4c43b75bba4861c907bb1.css">
<script src="/assets/modernizr-custom-cb807611a3e262b2eac59444cbab74d6.js" data-cfasync="false"></script>
<script src="/assets/js/jquery-3.1.js"></script>
<script src="/assets/js/jquery.rotate.js"></script>
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "Website",
  "publisher": "www.desgard.com",
  "url": "http://www.desgard.com/",
  "description": "瓜地"
}
</script>
<script type="text/javascript">
  var disqus_shortname = 'desgard',
      baseurl          = '';
</script>
<!--
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-52446115-1']);
_gaq.push(['_trackPageview']);
(function() {
  var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>-->
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "//hm.baidu.com/hm.js?c5a8123bc51782a3083a631ed9ff50e4";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script>
<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
      tex2jax: {
        inlineMath: [ ['$','$'] ],
        displayMath: [ ['$$','$$'] ],
        processEscapes: true
      }
    });
  </script>
  </head>
  <body class="post-template single">
    <header class="header">
  <div class="container">
    <h1><a href="/"><strong>desgard</strong>.com</a></h1>
    <nav class="navbar">
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="#!" data-modal="modalSearch"><i class="fa fa-search"></i></a></li>
        <li><a href="/comment " target="_blank"><i class="fa fa-comments"></i></a></li>
        <li><a href="/feed.xml" target="_blank"><i class="fa fa-feed"></i></a></li>
      </ul>
    </nav>
  </div>
</header>
    <main class="wrapper container" itemprop="mainContentOfPage" itemscope="itemscope" itemtype="http://schema.org/Blog">
      <article class="post" itemscope="itemscope" itemtype="http://schema.org/BlogPosting" itemprop="blogPost">
  <header class="post-header">
    <h1>BannerHoverView - 解耦 TableView Header 实现悬停</h1>
    <div class="post-meta">
      <time datetime="2015-10-13">
        <i class="fa fa-calendar-o"></i> <time datetime="2017-05-31"> 2017-05-31
      </time>
      <span>
        <i class="fa fa-comments"></i> <a href="#container">Comment me!</a>
      </span>
      <span>
<!--
        <i class="fa fa-folder-open-o"></i> 
-->
      </span>
    </div>
  </header>
  <p>邻近毕业，在完成毕设及论文之余，在帮助老师的创业公司写一个体育类 App。在其中遇到了这么一个需求，如下动图所示：</p>
<p><img src="http://7xwh85.com1.z0.glb.clouddn.com/%E6%9C%AA%E5%91%BD%E5%90%8D.gif" width="240px" /></p>
<p>其实在很多的 App 中都需要这样的样式，尤其在个人设置页面中需要让个人信息的 Banner 视图部分悬停在顶部作为一个 Navigation Bar 的占位从而合理的展示页面。如何流畅的展现这个滑动过程呢？这篇博文为解决该布局和页面需求做出一个实验性的探索。</p>
<h2 id="section">悬停效果</h2>
<p>对于悬停的第一反应，自然是想起了 TableView 的 Header 悬停。但是如果采取 Header 的部分视图悬停，在实现起来难度就稍微大一点（使 Section 的最后一个 Cell 与 Header 一起编写逻辑，给用户部分悬停的错觉即可）。</p>
<p>再来说一些状态的回调。我们希望在滑动过程中拿出这几种状态来进行处理，滑动到顶部状态、底部状态以及在滑动过程中的状态。</p>
<p>在 <strong>BannerHoverView</strong> 中，为了实现多种状态的识别我使用了 KVO 来对滑动的 offset 参数进行观察，从而得到特定时刻的滑动状态。而对于悬停的实现，来通过计算来确定最新的 frame 即可。下面来具体说明实现方法。</p>
<p><img src="http://7xwh85.com1.z0.glb.clouddn.com/banner-1.png" alt="banne" /></p>
<h2 id="bannerhoverview-">BannerHoverView 实现</h2>
<p>先来看一下参数属性：</p>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">static</span> <span class="kp">private</span> <span class="n">let</span> <span class="ss">eps</span><span class="p">:</span> <span class="no">CGFloat</span> <span class="o">=</span> <span class="mi">1</span><span class="n">e</span><span class="o">-</span><span class="mi">6</span>
<span class="kp">public</span> <span class="n">var</span> <span class="ss">headerScrollView</span><span class="p">:</span> <span class="no">UIScrollView</span><span class="o">!</span>
<span class="kp">public</span> <span class="n">var</span> <span class="ss">top</span><span class="p">:</span> <span class="no">CGFloat</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kp">public</span> <span class="n">fileprivate</span><span class="p">(</span><span class="n">set</span><span class="p">)</span> <span class="n">var</span> <span class="ss">bottom</span><span class="p">:</span> <span class="no">CGFloat</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kp">public</span> <span class="n">fileprivate</span><span class="p">(</span><span class="n">set</span><span class="p">)</span> <span class="n">var</span> <span class="ss">isTop</span><span class="p">:</span> <span class="no">Bool</span> <span class="o">=</span> <span class="kp">false</span>
<span class="kp">public</span> <span class="n">fileprivate</span><span class="p">(</span><span class="n">set</span><span class="p">)</span> <span class="n">var</span> <span class="ss">isBottom</span><span class="p">:</span> <span class="no">Bool</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">fileprivate</span> <span class="n">var</span> <span class="ss">completeBlock</span><span class="p">:</span> <span class="p">((</span><span class="no">BannerHoverView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="no">Void</span><span class="p">)?</span>
<span class="n">fileprivate</span> <span class="n">var</span> <span class="ss">startBlock</span><span class="p">:</span> <span class="p">((</span><span class="no">BannerHoverView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="no">Void</span><span class="p">)?</span>
<span class="n">fileprivate</span> <span class="n">var</span> <span class="ss">scrollBlock</span><span class="p">:</span> <span class="p">((</span><span class="no">BannerHoverView</span><span class="p">,</span> <span class="no">CGFloat</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="no">Void</span><span class="p">)?</span></code></pre></div>
<ul>
  <li><strong>eps</strong>：用于 double 类型判等方法。</li>
  <li><strong>headerScrollView</strong>：这是一个引用传递属性，用于将 View Controller 中的 Scroll View 传递进来。</li>
  <li><strong>top</strong>：悬停高度。</li>
  <li><strong>bottom</strong>：Banner 默认状态下总高度。</li>
  <li><strong>isTop</strong>：判断 Banner 是否为顶部状态。</li>
  <li><strong>isBottom</strong>：判断 Banner 是否为底部状态。</li>
  <li><strong>completeBlock</strong>：Banner 到达底部时候的回调闭包。</li>
  <li><strong>startBlock</strong>：Banner 到底顶部时候的回调闭包。</li>
  <li><strong>scrollBlock</strong>：滑动状态中的回调。<code>CGFloat</code> 参数为滑动进度，范围为 [0, 1] 闭区间。</li>
</ul>
<blockquote>
  <p>这里使用 eps 的原因是因为 double 的精度。不知道的可以感受一下下图出现的原因。在四则运算中，加减法对精度的影响较小，而乘法对精度的影响更大，除法最大。在 <strong>BannerHoverView</strong> 中因为关系到 alpha 值的改变，所以还是尽量保证精度问题。</p>
</blockquote>
<p><img src="http://7xwh85.com1.z0.glb.clouddn.com/14961963046903.jpg" alt="" /></p>
<h3 id="willmove-">willMove 方法</h3>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="kp">public</span> <span class="n">override</span> <span class="n">func</span> <span class="n">willMove</span><span class="p">(</span><span class="n">toWindow</span> <span class="ss">newWindow</span><span class="p">:</span> <span class="no">UIWindow</span><span class="sc">?)</span> <span class="p">{</span>
   <span class="k">super</span><span class="o">.</span><span class="n">willMove</span><span class="p">(</span><span class="ss">toWindow</span><span class="p">:</span> <span class="n">newWindow</span><span class="p">)</span>
   <span class="n">headerScrollView</span><span class="o">.</span><span class="n">contentInset</span> <span class="o">=</span> <span class="no">UIEdgeInsets</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">top</span><span class="p">:</span> <span class="n">bottom</span><span class="p">,</span> <span class="ss">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">bottom</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">right</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
   <span class="sr">//</span> <span class="err">感谢</span> <span class="vi">@Josscii</span> <span class="err">的</span> <span class="no">PR</span>
   <span class="n">headerScrollView</span><span class="o">.</span><span class="n">scrollIndicatorInsets</span> <span class="o">=</span> <span class="no">UIEdgeInsets</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">top</span><span class="p">:</span> <span class="n">bottom</span><span class="p">,</span> <span class="ss">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">bottom</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">right</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">}</span></code></pre></div>
<p><code>willMove</code> 这个方法即为 <code>willMoveToWindow</code>。在 View Controller 的 <code>viewWillAppear</code> 周期方法中会调用其子视图中该方法。而在 <strong>BannerHoverView</strong> 中，这个时机十分适合设置 Table View 的上下偏移，以及 Indicator 的上下偏移。由于其 bottom 属性已经在初始化的时候确定。</p>
<h2 id="kvo--offset-">用 KVO 对 Offset 进行监听的核心部分</h2>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="sr">//</span> <span class="ss">MARK</span><span class="p">:</span> <span class="o">-</span> <span class="no">KVO</span>
<span class="n">override</span> <span class="n">func</span> <span class="n">observeValue</span><span class="p">(</span><span class="n">forKeyPath</span> <span class="ss">keyPath</span><span class="p">:</span> <span class="nb">String</span><span class="sc">?,</span> <span class="n">of</span> <span class="ss">object</span><span class="p">:</span> <span class="no">Any</span><span class="sc">?,</span> <span class="ss">change</span><span class="p">:</span> <span class="o">[</span><span class="no">NSKeyValueChangeKey</span> <span class="p">:</span> <span class="no">Any</span><span class="o">]</span><span class="sc">?,</span> <span class="ss">context</span><span class="p">:</span> <span class="no">UnsafeMutableRawPointer</span><span class="sc">?)</span> <span class="p">{</span>
   <span class="k">if</span> <span class="n">let</span> <span class="kp">new</span> <span class="o">=</span> <span class="n">change?</span><span class="o">[</span><span class="no">NSKeyValueChangeKey</span><span class="o">.</span><span class="n">newKey</span><span class="o">]</span> <span class="p">{</span>
       <span class="n">let</span> <span class="n">point</span> <span class="o">=</span> <span class="p">(</span><span class="kp">new</span> <span class="n">as!</span> <span class="no">NSValue</span><span class="p">)</span><span class="o">.</span><span class="n">cgPointValue</span>
       <span class="n">updateSubViewsWithScrollOffset</span><span class="p">(</span><span class="ss">newOffset</span><span class="p">:</span> <span class="n">point</span><span class="p">)</span>
   <span class="p">}</span>
<span class="p">}</span></code></pre></div>
<p>在 <code>observeValue</code> 方法中，我需要获得的是 Table View 的 <code>contentOffset</code> 属性，拿到的是一个 Point 对象。然后根据这个新的坐标，来更新视图操作，即调用 <code>updateSubViewsWithScrollOffset</code> 方法。</p>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">fileprivate</span> <span class="n">func</span> <span class="n">updateSubViewsWithScrollOffset</span><span class="p">(</span><span class="ss">newOffset</span><span class="p">:</span> <span class="no">CGPoint</span><span class="p">)</span> <span class="p">{</span>
   <span class="n">var</span> <span class="n">newOffset</span> <span class="o">=</span> <span class="n">newOffset</span>
   <span class="sr">//</span> <span class="err">取出额外滚动区域的顶部位置，然后取反</span>
   <span class="n">let</span> <span class="n">startChangeOffset</span> <span class="o">=</span> <span class="o">-</span><span class="n">headerScrollView</span><span class="o">.</span><span class="n">contentInset</span><span class="o">.</span><span class="n">top</span>
   <span class="sr">//</span> <span class="err">计算滑动点，通过</span> <span class="n">top</span> <span class="err">和</span> <span class="n">startChangeOffset</span> <span class="err">来确定滑动区域</span>
   <span class="n">newOffset</span> <span class="o">=</span> <span class="no">CGPoint</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">x</span><span class="p">:</span> <span class="n">newOffset</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="ss">y</span><span class="p">:</span> <span class="n">newOffset</span><span class="o">.</span><span class="n">y</span> <span class="o">&lt;</span> <span class="n">startChangeOffset</span> <span class="p">?</span> <span class="n">startChangeOffset</span> <span class="p">:</span> <span class="n">min</span><span class="p">(</span><span class="n">newOffset</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="o">-</span><span class="n">top</span><span class="p">))</span>
   <span class="sr">//</span> <span class="err">根据滑动点范围确定</span> <span class="n">frame</span> <span class="err">的</span> <span class="n">y</span> <span class="err">坐标</span>
   <span class="n">let</span> <span class="n">newY</span> <span class="o">=</span> <span class="o">-</span><span class="n">newOffset</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">bottom</span>
   <span class="sr">//</span> <span class="err">根据滑动确定</span> <span class="n">frame</span> <span class="err">新值</span>
   <span class="n">frame</span> <span class="o">=</span> <span class="no">CGRect</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">y</span><span class="p">:</span> <span class="n">newY</span><span class="p">,</span> <span class="ss">width</span><span class="p">:</span> <span class="n">frame</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="ss">height</span><span class="p">:</span> <span class="n">frame</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
   <span class="sr">//</span> <span class="err">计算总滑动距离</span>
   <span class="n">let</span> <span class="n">distance</span> <span class="o">=</span> <span class="o">-</span><span class="n">top</span> <span class="o">-</span> <span class="n">startChangeOffset</span>
   <span class="sr">//</span> <span class="err">计算滑动距离百分比</span>
   <span class="n">let</span> <span class="n">percent</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">-</span> <span class="p">(</span><span class="n">newOffset</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">startChangeOffset</span><span class="p">)</span> <span class="o">/</span> <span class="n">distance</span>
   <span class="sr">//</span> <span class="err">回调处理部分，更新状态</span>
   <span class="k">if</span> <span class="mi">1</span><span class="o">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">percent</span> <span class="o">&gt;</span> <span class="no">BannerHoverView</span><span class="o">.</span><span class="n">eps</span> <span class="o">&amp;&amp;</span> <span class="n">percent</span> <span class="o">-</span> <span class="mi">0</span><span class="o">.</span><span class="mi">0</span> <span class="o">&gt;</span> <span class="no">BannerHoverView</span><span class="o">.</span><span class="n">eps</span> <span class="p">{</span>
       <span class="n">isBottom</span> <span class="o">=</span> <span class="kp">false</span>
       <span class="n">isTop</span> <span class="o">=</span> <span class="kp">false</span>
   <span class="p">}</span>
   <span class="k">else</span> <span class="k">if</span> <span class="n">isBottom</span> <span class="o">==</span> <span class="kp">false</span> <span class="o">&amp;&amp;</span> <span class="n">isTop</span> <span class="o">==</span> <span class="kp">false</span> <span class="p">{</span>
       <span class="k">if</span> <span class="mi">1</span><span class="o">.</span><span class="mi">0</span> <span class="o">-</span> <span class="n">percent</span> <span class="o">&lt;</span> <span class="no">BannerHoverView</span><span class="o">.</span><span class="n">eps</span> <span class="p">{</span>
           <span class="n">isTop</span> <span class="o">=</span> <span class="kp">true</span>
           <span class="k">if</span> <span class="n">let</span> <span class="n">topAction</span> <span class="o">=</span> <span class="n">completeBlock</span> <span class="p">{</span>
               <span class="n">topAction</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
           <span class="p">}</span>
       <span class="p">}</span>
       <span class="k">else</span> <span class="k">if</span> <span class="n">percent</span> <span class="o">-</span> <span class="mi">0</span><span class="o">.</span><span class="mi">0</span> <span class="o">&lt;</span> <span class="no">BannerHoverView</span><span class="o">.</span><span class="n">eps</span> <span class="p">{</span>
           <span class="n">isBottom</span> <span class="o">=</span> <span class="kp">true</span>
           <span class="k">if</span> <span class="n">let</span> <span class="n">bottomAction</span> <span class="o">=</span> <span class="n">startBlock</span> <span class="p">{</span>
               <span class="n">bottomAction</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
           <span class="p">}</span>
       <span class="p">}</span>
   <span class="p">}</span>
   <span class="sr">//</span> <span class="err">调用滑动时期闭包方法</span>
   <span class="k">if</span> <span class="n">let</span> <span class="n">scrollAction</span> <span class="o">=</span> <span class="n">scrollBlock</span> <span class="p">{</span>
       <span class="n">scrollAction</span><span class="p">(</span><span class="nb">self</span><span class="p">,</span> <span class="n">percent</span><span class="p">)</span>
   <span class="p">}</span>
<span class="p">}</span></code></pre></div>
<p>在更新滑动的操作中，无非就是在做两件事情：</p>
<ol>
  <li>判断滑动坐标是否越界（bottom 和 top 参数进行限制）。</li>
  <li>判断滑动状态以触发不同状态的回调方法。</li>
</ol>
<p>这大概 30 行左右的代码就是 <strong>BannerHoverView</strong> 的核心部分，感觉较为精简。如果有更好的实现方法，欢迎 PR。</p>
<h2 id="view-controller--bannerhoverview">在 View Controller 初始化 BannerHoverView</h2>
<p>你只需要三步就可以初始化 BannerHoverView 并使用：</p>
<ul>
  <li>对其中的 <code>headerScrollView</code> 进行关联：</li>
</ul>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="sr">//</span> <span class="no">TableView</span> <span class="no">Initial</span>
<span class="n">tableView</span> <span class="o">=</span> <span class="no">UITableView</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">frame</span><span class="p">:</span> <span class="n">view</span><span class="o">.</span><span class="n">bounds</span><span class="p">,</span> <span class="ss">style</span><span class="p">:</span> <span class="o">.</span><span class="n">grouped</span><span class="p">)</span>
<span class="n">tableView</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="nb">self</span>
<span class="n">tableView</span><span class="o">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="nb">self</span>
<span class="sr">//</span> <span class="no">BannerHoverView</span> <span class="no">Initial</span>
<span class="n">bannerHoverView</span> <span class="o">=</span> <span class="no">SampleView</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">frame</span><span class="p">:</span> <span class="no">CGRect</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">width</span><span class="p">:</span> <span class="n">view</span><span class="o">.</span><span class="n">frame</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="ss">height</span><span class="p">:</span> <span class="mi">280</span><span class="p">))</span>
<span class="sr">//</span> <span class="no">Hover</span> <span class="no">Height</span><span class="p">(</span><span class="no">Remaining</span> <span class="n">part</span> <span class="k">when</span> <span class="no">BannerHoverView</span> <span class="n">arrived</span> <span class="n">at</span> <span class="n">the</span> <span class="n">top</span> <span class="n">position</span><span class="p">)</span>
<span class="n">bannerHoverView</span><span class="o">.</span><span class="n">top</span> <span class="o">=</span> <span class="mi">65</span>
<span class="o">//</span> <span class="no">Scroll</span> <span class="no">Property</span> <span class="no">Setting</span>
<span class="n">bannerHoverView</span><span class="o">.</span><span class="n">headerScrollView</span> <span class="o">=</span> <span class="n">tableView</span>
<span class="sr">//</span> <span class="no">Add</span> <span class="no">Observer</span>
<span class="n">tableView</span><span class="o">.</span><span class="n">addObserver</span><span class="p">(</span><span class="n">bannerHoverView</span><span class="p">,</span> <span class="ss">forKeyPath</span><span class="p">:</span> <span class="s2">&quot;contentOffset&quot;</span><span class="p">,</span> <span class="ss">options</span><span class="p">:</span> <span class="no">NSKeyValueObservingOptions</span><span class="o">.</span><span class="n">new</span><span class="p">,</span> <span class="ss">context</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
<span class="n">view</span><span class="o">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">tableView</span><span class="p">)</span>
<span class="n">view</span><span class="o">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">bannerHoverView</span><span class="p">)</span></code></pre></div>
<ul>
  <li>在 deinit 中删除 KVO 监听</li>
</ul>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">deinit</span> <span class="p">{</span>
    <span class="n">tableView</span><span class="o">.</span><span class="n">removeObserver</span><span class="p">(</span><span class="n">bannerHoverView</span><span class="p">,</span> <span class="ss">forKeyPath</span><span class="p">:</span> <span class="s2">&quot;contentOffset&quot;</span><span class="p">)</span>
<span class="p">}</span></code></pre></div>
<ul>
  <li>继承 <code>BannerHoverView</code> 来定制悬停部分视图：</li>
</ul>
<div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">SampleView</span><span class="p">:</span> <span class="no">BannerHoverView</span> <span class="p">{</span>
    <span class="n">override</span> <span class="n">init</span><span class="p">(</span><span class="ss">frame</span><span class="p">:</span> <span class="no">CGRect</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">super</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="ss">frame</span><span class="p">:</span> <span class="n">frame</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">required</span> <span class="n">init?</span><span class="p">(</span><span class="n">coder</span> <span class="ss">aDecoder</span><span class="p">:</span> <span class="no">NSCoder</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fatalError</span><span class="p">(</span><span class="s2">&quot;init(coder:) has not been implemented&quot;</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nb">self</span><span class="o">.</span><span class="n">setScrollAction</span> <span class="p">{</span> <span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="n">offset</span><span class="p">)</span> <span class="k">in</span>
        <span class="sr">//</span> <span class="n">offset</span> <span class="o">-</span> <span class="n">distance</span> <span class="n">percent</span>
    <span class="p">}</span>
    <span class="nb">self</span><span class="o">.</span><span class="n">setTopAction</span> <span class="p">{</span> <span class="p">(</span><span class="n">view</span><span class="p">)</span> <span class="k">in</span>
        <span class="sr">//</span> <span class="n">scroll</span> <span class="n">top</span> <span class="n">callback</span>
    <span class="p">}</span>
    <span class="nb">self</span><span class="o">.</span><span class="n">setBottomAction</span> <span class="p">{</span> <span class="p">(</span><span class="n">view</span><span class="p">)</span> <span class="k">in</span>
        <span class="sr">//</span> <span class="n">scroll</span> <span class="n">bottom</span> <span class="n">callback</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></div>
<p>以上便是 <strong>BannerHoverView</strong> 的实现思路，希望这个思路可以给大家启发，也希望得到更好的解决办法，在讨论中学习最优雅的实现方式。</p>
<blockquote>
  <p>Github：<a href="https://github.com/Desgard/BannerHoverView">https://github.com/Desgard/BannerHoverView</a></p>
</blockquote>
  <footer class="post-footer">
    <section class="author">
      <h4>Desgard_Duan</h4>
      <p>I write many code to write less code.💻</p>
    </section>
<aside class="share">
  <h4>Share this.</h4>
  <a href="http://twitter.com/share?text=《BannerHoverView - 解耦 TableView Header 实现悬停》 -- Guardia · 瓜地&amp;url=https://www.desgard.com/BannerHoverView/"
  onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;">
    <i class="fa fa-twitter-square"></i>
  </a>
  <a href="http://v.t.sina.com.cn/share/share.php?url=https://www.desgard.com/BannerHoverView/&amp;title=《BannerHoverView - 解耦 TableView Header 实现悬停》 —— Guardia · 瓜地" onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;">
    <i class="fa fa-weibo"></i>
  </a>
</aside>
      <hr>
<aside id="comments" class="disqus">
  <div id="container"></div>
  <!-- <link rel="stylesheet" href="/assets/css/gitment.css">
  <script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
  <script>
  var gitment = new Gitment({
    id: "https://www.desgard.com/BannerHoverView/", 
    owner: 'Desgard',
    repo: 'desgard.github.com',
    oauth: {
      client_id: 'e2612df42f3f2a83e71c',
      client_secret: 'b53e85b314bb24a6d06773e48bbb62a4de3b8b3a',
    },
  })
  gitment.render('container')
  </script> -->
<link rel="stylesheet" href="/assets/css/gitalk.css">
<script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>
<div id='gitalk-container'></div>
<script>
    const gitalk = new Gitalk({
        id: "https://www.desgard.com/BannerHoverView/", 
        clientID: 'e2612df42f3f2a83e71c',
        clientSecret: 'b53e85b314bb24a6d06773e48bbb62a4de3b8b3a',
        repo: 'desgard.github.com',
        owner: 'Desgard',
        admin: ['Desgard'],
        // facebook-like distraction free mode
        distractionFreeMode: false
    })
    gitalk.render('gitalk-container')
</script>
</aside>
  </footer>
</article>
    </main>
<footer class="footer">
  <div class="container">
    <ul class="icons">
      <li>
        <a href="https://github.com/desgard" class="icon-github" target="_blank">
          <i class="fa fa-github"></i>
        </a>
      </li>
      <li>
        <a href="https://www.facebook.com/desgard.duan" class="icon-facebook" target="_blank">
          <i class="fa fa-facebook"></i>
        </a>
      </li>
      <li>
        <a href="https://twitter.com/Desgard_Duan" class="icon-twitter" target="_blank">
          <i class="fa fa-twitter"></i>
        </a>
      </li>
      <li>
        <a href="https://stackoverflow.com/users/6119149/desgard-duan" class="icon-github" target="_blank">
          <i class="fa fa-stack-overflow"></i>
        </a>
      </li>
      <li>
        <a href="https://weibo.com/desgard" class="icon-instagram" target="_blank">
          <i class="fa fa-weibo"></i>
        </a>
      </li>
    </ul>
    <p>
      <q>I write many code to write less code.💻</q>
      <small>– Gua</small>
    </p>
    <small class="clearfix">
      Powered by <a href="http://jekyllrb.com" target="_blank">Jekyll</a> • <a href="https://github.com/desgard" target="_blank">Open source <i class="fa fa-heart"></i></a>
    </small>
  </div>
</footer>
<a class="scroll-up fa fa-chevron-up bounce" href="#" data-position="0"></a>
<div id="modalSearch" class="modal">
  <div class="modal__overlay"></div>
  <div class="modal__content">
    <a href="#!" class="modal-close" data-modal-close>&times;</a>
    <div class="search-container">
      <input type="text" id="search-input" placeholder="Search articles">
      <ul id="results-container"></ul>
    </div>
  </div>
</div>
    <script src="/assets/main-52d417e8a6ff9f5b168386d37c96338a.js"></script>
  </body>
  <script>
    var link = "" ;
    var os = function() {  
      var ua = navigator.userAgent,  
      isWindowsPhone = /(?:Windows Phone)/.test(ua),  
      isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone,   
      isAndroid = /(?:Android)/.test(ua),   
      isFireFox = /(?:Firefox)/.test(ua),   
      isChrome = /(?:Chrome|CriOS)/.test(ua),  
      isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)),  
      isPhone = /(?:iPhone)/.test(ua) && !isTablet,  
      isPc = !isPhone && !isAndroid && !isSymbian;  
      return {  
        isTablet: isTablet,  
        isPhone: isPhone,  
        isAndroid : isAndroid,  
        isPc : isPc  
      };  
    }();  
    if (link.length > 0) {
      if (os.isAndroid || os.isPhone) {
        location.replace(link);
      }
    }
  </script>
</html>